/*
 *  Copright: 2008 Philipp Leitner, Distributed Systems Group,
 *            TU Wien, leitner@infosys.tuwien.ac.at
 *
 *  This file is part of DAIOS.
 *
 *  DAIOS is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as
 *  published by the Free Software Foundation.
 * 
 *  DAIOS is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with DAIOS.  If not, see
 *  <http://www.gnu.org/licenses/>.
 * 
 */

package at.ac.tuwien.infosys.dsg.daiosPlugins.axis2.axiom;

import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Logger;

import javax.xml.stream.XMLStreamException;

import org.apache.axiom.om.OMAbstractFactory;
import org.apache.axiom.om.OMAttribute;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMFactory;
import org.apache.axiom.om.OMNamespace;
import org.apache.axiom.om.OMText;
import org.apache.commons.collections.iterators.ArrayIterator;

import at.ac.tuwien.infosys.dsg.daios.exceptions.AmbigiousException;
import at.ac.tuwien.infosys.dsg.daios.exceptions.ArrayException;
import at.ac.tuwien.infosys.dsg.daios.exceptions.TypeErrorException;
import at.ac.tuwien.infosys.dsg.daios.exceptions.UnsupportedClassException;
import at.ac.tuwien.infosys.dsg.daios.framework.interfaces.IConstants;
import at.ac.tuwien.infosys.dsg.daios.framework.structs.DaiosMessage;
import at.ac.tuwien.infosys.dsg.daios.framework.structs.DaiosOutputMessage;
import at.ac.tuwien.infosys.dsg.daios.util.Utility;
import at.ac.tuwien.infosys.dsg.daios.wsdl.AtomicTypesMapper;

/**
 * The converter used for converting Daios messages to OM XML models in
 * case of REST invocations through the Axis2 backend.
 * 
 * @author philipp
 *
 */
public class OMElementRESTConverter {
	
	private Logger log = null;
	private OMFactory fac = null;
	
	/**
	 * Private constructor. Instances of this converter should
	 * be accessed only via getInstance() .
	 *
	 */
	private OMElementRESTConverter() {
		log = Logger.getLogger(IConstants.LOG_NAME);
		fac = OMAbstractFactory.getOMFactory();
	}
	
	/**
	 * The standard factory method for this converter.
	 * 
	 * @return A new instance of RESTConverter.
	 */
	public static OMElementRESTConverter getInstance() {
		return new OMElementRESTConverter();
	}
	
	/**
	 * Convert an asynchronously received document into a Daios message.
	 * For some reason we sometimes receive different messages from
	 * Axis2 depending on whether we use synchronous or asynchronous
	 * invocations. This might be a bug in Axis2.
	 * 
	 * @param soapMsg The asynchronous result to convert
	 * @return The converted Daios message.
	 */
	public DaiosOutputMessage convertAsync(OMElement soapMsg) {
		
		return convert(OMUtil.getPayload(OMUtil.getBody(soapMsg)));
		
	}
	
	/**
	 * Converts a DaiosInputMessage to XML. The mapping is controlled
	 * by an 'example' (REST by example).
	 * 
	 * @param message The input message to convert.
	 * @param example The example request
	 * @return An XML message as OM element. The structure of this XML
	 * document is the same as in the given example, but the actual
	 * values are taken from the input message.
	 * @throws AmbigiousException Thrown to indicate that it is not
	 * possible to convert this input message according to the given
	 * example.
	 */
	public OMElement convert(DaiosMessage message, OMElement example)
		throws AmbigiousException {
		
		// first we need to create a root element
		// (this is always the same as in the example)
		OMNamespace rootNS =
			fac.createOMNamespace(example.getQName().getNamespaceURI(),
					example.getQName().getPrefix());
		OMElement rootOMElement =
			fac.createOMElement(example.getQName().getLocalPart(), rootNS);
		
		// now we iterate over all first-level children of the example message
		// and add the appropriate value from the Daios message
		Iterator children = example.getChildElements();
		OMElement thisNode = null;
		OMElement lastNode = null;
		while(children.hasNext()) {
			thisNode = (OMElement) children.next();
			if(thisNode.getQName().getLocalPart() != null) {
				
				// if we encounter a child with the same name as its
				// previous sibling we just ignore it (this is
				// an array type)
				if(lastNode != null && lastNode.getQName().getLocalPart().equals(
									  thisNode.getQName().getLocalPart())) {
					lastNode = thisNode;
					continue;
				}
				
				appendSubelement(rootOMElement, thisNode, message);
				lastNode = thisNode;
			}
		}
		
		// after going through all subelements the Daios message should be empty
		// - if it is not the structure of the messages were not compatible
		if(message.getNrOfFields() > 0)
			throw new AmbigiousException("Your message is incompatible with the given "+
				"XML example. "+message.getNrOfFields()+" fields could not be found "+
				"in the example.");
		
		// return the manufactured OMElement
		return rootOMElement;
		
	}

	/**
	 * Converts a XML response encoded as AXIOM model
	 * into a DaiosOutputMessage. Since we do not have
	 * any information about the structure of this response
	 * the following mapping rules apply:
	 * 
	 * <pre>
	 *   -- Simple fields are returned as String
	 *   -- Simple array fields are returned as String[]
	 *   -- Complex fields are returned as DaiosMessage
	 *   -- Complex array fields are returned as DaiosMessage[]
	 * </pre>
	 * 
	 * @param returnMessage The input document as OMElement
	 * @return The converted DaiosMessage
	 * @throws XMLStreamException
	 */
	public DaiosOutputMessage convert(OMElement returnMessage) {
		
		DaiosOutputMessage m = new DaiosOutputMessage();
		
		extractAttributes(m,returnMessage);
		
		Iterator it = returnMessage.getChildren();
		int usedCounter = 0;
		Object o = null;
		OMElement el = null;
		boolean isSimple = false;
		boolean isArray = false;
		while(it.hasNext()) {
			o = it.next();
			if(o instanceof OMElement)
				el = (OMElement) o;
			else {
				//  ignore text content
				continue;
			}
			if(usedCounter > 1) {
				usedCounter--;
				if(it.hasNext())
					it.next();
				continue;
			}
			isSimple = checkIfSimple(el);
			isArray = checkIfArray(el);
			// we have a simple type
			if(isSimple && !isArray) {
				extractSimpleValue(m, el);
			}
			// we have an array of simple types
			else if(isSimple && isArray) {
				usedCounter = extractArrayValues(
					m, el);
			}
			// we have a complex type
			else if(!isSimple && !isArray){
				extractComplexValue(m, el);
			}
			// we have a complex type array
			else {
				usedCounter = extractComplexArrayValues(
						m, el);				
			}
				
		}
		
		return m;
		
	}

	/**
	 * Append a OM element to a root element, where the structure
	 * of the new element is defined by an example and the content
	 * by a Daios message.
	 * 
	 * @param rootOMElement The root element where the new OM element
	 * should be inserted as child.
	 * @param node The formatting example.
	 * @param message The message that delivers the actual value.
	 * @return In case of an array return the number of array elements
	 * processed.
	 * @throws AmbigiousException
	 */
	private int appendSubelement(OMElement rootOMElement, OMElement node,
		DaiosMessage message) throws AmbigiousException {
		
		int arraysize = 0;
		
		Object DaiosValue = message.get(node.getLocalName());
		
		if(DaiosValue == null)
			appendEmptyElement(rootOMElement, node);
		else {
			
			boolean isSimple = message.isSimple(node.getLocalName());
			boolean isArray = message.isArray(node.getLocalName());
			
			// check if the Daios value is simple, array or complex
			if(isSimple && !isArray)
				appendSimpleElement(rootOMElement, node, DaiosValue);
			else if(isSimple && isArray)
				arraysize = appendArrayElement(rootOMElement, node, DaiosValue);
			else if(!isSimple && !isArray)
				appendComplexElement(rootOMElement, node, (DaiosMessage)DaiosValue);
			else
				appendComplexArrayElement(rootOMElement, node, (DaiosMessage[])DaiosValue);
			
			// remove the already processed field
			message.remove(node.getLocalName());
			
		}
		
		return arraysize;
		
	}

	/**
	 * Append a simple OM element to a root element, where the structure
	 * of the new element is defined by an example and the content
	 * is a simple value.
	 * 
	 * @param rootOMElement The root element where the new OM element
	 * should be inserted as child.
	 * @param node The formatting example.
	 * @param DaiosValue The actual value to insert.
	 */
	private void appendSimpleElement(OMElement rootOMElement, OMElement node,
			Object DaiosValue) {
		
		OMNamespace ns = fac.createOMNamespace(node.getQName().getNamespaceURI(),
				node.getQName().getPrefix());
		OMElement el = fac.createOMElement(node.getLocalName(), ns);
		OMText text = fac.createOMText(el, AtomicTypesMapper.encodeForWire(DaiosValue));
		el.addChild(text);
		rootOMElement.addChild(el);
		
	}

	/**
	 * Append a OM element to a root element, where the structure
	 * of the new element is defined by an example and the content
	 * is an array of simple values.
	 * 
	 * @param rootOMElement The root element where the new OM element
	 * should be inserted as child.
	 * @param node The formatting example.
	 * @param DaiosValue The array that we are going to insert as
	 * java.lang.Object.
	 * @return The number of array elements processed.
	 * @throws AmbigiousException
	 */
	private int appendArrayElement(OMElement rootOMElement, OMElement node,
			Object DaiosValue) {
		
		int arraysize = 0;
		
		// iterate through the array and add all values to the message
		// (we can treat all array vals as simple types)
		ArrayIterator it = new ArrayIterator(DaiosValue);
		
		Object o = null;
		while(it.hasNext()) {
			arraysize++;
			o = it.next();
			appendSimpleElement(rootOMElement, node, o);
		}
		
		return arraysize;
	}
	
	/**
	 * Append a OM element to a root element, where the structure
	 * of the new element is defined by an example and the content
	 * is a Daios message.
	 * 
	 * @param rootOMElement The root element where the new OM element
	 * should be inserted as child.
	 * @param node The formatting example.
	 * @param message The actual value.
	 * @throws AmbigiousException
	 */
	private void appendComplexElement(OMElement rootOMElement, OMElement node,
			DaiosMessage message) throws AmbigiousException {
		
		// go one step down recursively
		OMElement subelement = convert(message, node);
		rootOMElement.addChild(subelement);
		
	}

	/**
	 * Append a OM element to a root element, where the structure
	 * of the new element is defined by an example and the content
	 * is an array of Daios messages.
	 * 
	 * @param rootOMElement The root element where the new OM element
	 * should be inserted as child.
	 * @param node The formatting example.
	 * @param messages The array of actual values (messages).
	 * @return The number of array elements processed.
	 * @throws AmbigiousException
	 */
	private int appendComplexArrayElement(OMElement rootOMElement, OMElement node,
			DaiosMessage[] messages) throws AmbigiousException {
		
		// iterate through the array and add all values to the message
		for(DaiosMessage m: messages)
			appendComplexElement(rootOMElement, node, m);
		
		return messages.length;
			
	}
	
	/**
	 * Append an empty OM element to a root element.
	 * 
	 * <p>
	 * 
	 * The formatting example is only used for the namespace
	 * URI here.
	 * 
	 * @param rootOMElement The root element where the new OM element
	 * should be inserted as child.
	 * @param node The formatting example.
	 */
	private void appendEmptyElement(OMElement rootOMElement, OMElement node) {
		
		OMNamespace ns = fac.createOMNamespace(node.getQName().getNamespaceURI(),
				node.getQName().getPrefix());
		OMElement el = fac.createOMElement(node.getLocalName(), ns);
		rootOMElement.addChild(el);
		
	}
	
	/**
	 * Extract a simple value from the XML model and insert it into
	 * the output message.
	 * 
	 * @param m The output message to insert into.
	 * @param el The simple value as AXIOM model.
	 */
	private void extractSimpleValue(DaiosOutputMessage m, OMElement el) {
		
		try {
			
			String val = el.getText();
			Class vClass = AtomicTypesMapper.getDefaultClass();
			Object castedVal
				= AtomicTypesMapper.construct(val,vClass);
			
			m.set(el.getLocalName(),castedVal,vClass);
			
		} catch (IOException e) {
			log.severe("Internal Daios error while processing "+
				"the server response.");
			e.printStackTrace();
		} catch (UnsupportedClassException e) {
			log.severe("Internal Daios error while processing "+
				"the server response.");
			e.printStackTrace();
		} catch (TypeErrorException e) {
			log.severe("Internal Daios error while processing "+
				"the server response.");
			e.printStackTrace();
		} catch (ArrayException e) {
			log.severe("Internal Daios error while processing "+
				"the server response.");
			e.printStackTrace();
		}
	}

	/**
	 * Extract an array of simple values from the XML model and insert them
	 * as array value into the output message.
	 * 
	 * @param m The output message to insert into.
	 * @param el The simple array value as AXIOM model.
	 * @return The number of array values extracted.
	 */
	private int extractArrayValues(DaiosOutputMessage m, OMElement el) {
		
		List<Object> l = new LinkedList<Object>();
		
		String name = el.getLocalName();

		int elCounter = 0;
		
		// get all array values in a list
		for(OMElement e = el; e != null && e.getLocalName().equals(name);
			e = OMUtil.getNextOMElement(e)) {
			
			elCounter++;
			
			String val = e.getText();
			
			// ignore empty array values
			if(val == null || val.trim().equals(""))
				continue;
			
			Class vClass = AtomicTypesMapper.getDefaultClass();
			Object castedVal = null;
			try {
				castedVal = AtomicTypesMapper.construct(val,vClass);
			} catch (IOException exc) {
				log.severe("Internal Daios error while processing "+
					"the server response.");
				exc.printStackTrace();
			} catch (UnsupportedClassException exc) {
				log.severe("Internal Daios error while processing "+
					"the server response.");
				exc.printStackTrace();
			} 
			l.add(castedVal);
		}
		
		// now add this array to the message
		try {
			m.set(name, Utility.convert(l));
		} catch (ArrayException e) {
			log.severe("Internal Daios error while processing "+
				"the server response.");
			e.printStackTrace();
		}
		
		// return the number of elements we have used
		// (so that they are not used again)
		return elCounter;
	}

	/**
	 * Extract an array of complex values from the XML model and insert them
	 * as array of DaiosMessages into the output message.
	 * 
	 * @param m The output message to insert into.
	 * @param el The complex array value as AXIOM model.
	 * @return The number of array elements extracted.
	 */	
	private int extractComplexArrayValues(DaiosOutputMessage m, OMElement el) {
		
		List<DaiosMessage> l = new LinkedList<DaiosMessage>();
		
		String name = el.getLocalName();

		int elCounter = 0;
		
		// get all array values in a list
		for(OMElement e = el; e != null && e.getLocalName().equals(name);
			e = OMUtil.getNextOMElement(e)) {
			
			elCounter++;
			
			l.add(convert(e));
			
		}
		
		// now add this array to the message
		DaiosMessage[] messages = Utility.convert(l);
		try {
			m.setComplexArray(name, messages);
		} catch (ArrayException e) {
			log.severe("Internal Daios error while processing "+
				"the server response.");
			e.printStackTrace();
		}
		
		// return the number of elements we have used
		// (so that they are not used again)
		return elCounter;
	}
	
	/**
	 * Extract a complex value from the XML model and insert it
	 * as DaiosMessage into the output message.
	 * 
	 * @param m The output message to insert into.
	 * @param el The complex value as AXIOM model.
	 */		
	private void extractComplexValue(DaiosOutputMessage m, OMElement el) {
		// recursively go deeper
		m.setComplex(el.getLocalName(), convert(el));
	}

	/**
	 * Extract attributes from the XML model and insert them
	 * as fields into the output message.
	 * 
	 * @param m The output message to insert into.
	 * @param el The complex value as AXIOM model.
	 */		
	private void extractAttributes(DaiosMessage m, OMElement el) {
		
		Iterator it = el.getAllAttributes();
		Object o = null;
		OMAttribute att = null;
		while(it.hasNext()) {
			o = it.next();
			if(o instanceof OMAttribute) {
				att = (OMAttribute)o;
				try {
					m.set(att.getLocalName(), att.getAttributeValue());
				} catch (ArrayException e) {
					log.severe("Internal Daios error while processing "+
						"the server response.");
					e.printStackTrace();
				}
			}
		}
		
	}
	
	/**
	 * Check if a certain AXIOM construct represents a simple value.
	 * 
	 * <p>
	 * 
	 * We consider a value 'simple' if it has no attributes and
	 * no subelements.
	 * 
	 * 
	 * @param el The element to check.
	 * @return True if the element is simple, false otherwise.
	 */	
	private boolean checkIfSimple(OMElement el) {
		
		if(el.getFirstElement() != null)
			return false;
		
		if(el.getAllAttributes().hasNext())
			return false;
		
		return true;
		
	}
	
	/**
	 * Check if a certain AXIOM construct represents an array value.
	 * 
	 * <p>
	 * 
	 * An element is considered an 'array' if it has a
	 * previous <i>or</i> a next OM sibling with the same
	 * name.
	 * 
	 * @param el The element to check.
	 * @return True if the element is an array, false otherwise.
	 */
	private boolean checkIfArray(OMElement el) {
		
		String name = el.getLocalName();
		
		OMElement previous = OMUtil.getPreviousOMElement(el);
		if(previous != null && previous.getLocalName().equals(name))
			return true;
		
		OMElement next = OMUtil.getNextOMElement(el);
		if(next != null && next.getLocalName().equals(name))
			return true;
		
		return false;
	}

}
